﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Reflection;
using System.Diagnostics;
using System.Xml.Serialization;
using MethodWorx.Core.Extensions;
using MethodWorx.Core.Serialization;
using System.Collections;

namespace System
{
    /// <summary>
    /// Extension methods for the System.Object type
    /// </summary>
    public static class ObjectExtensions
    {
        private static Dictionary<string, XmlSerializer> formatters = new Dictionary<string, XmlSerializer>();
        private sealed class AllowAllAssemblyVersionsDeserializationBinder : System.Runtime.Serialization.SerializationBinder
        {
            private Assembly parent;

            public AllowAllAssemblyVersionsDeserializationBinder(Assembly parent)
            {
                this.parent = parent;
            }
            public override Type BindToType(string assemblyName, string typeName)
            {
                Type typeToDeserialize = null;

                String currentAssembly = this.parent.FullName;

                // Always use the currently assembly
                assemblyName = currentAssembly;

                // Get the type using the typeName and assemblyName
                typeToDeserialize = Type.GetType(String.Format("{0}, {1}",
                    typeName, assemblyName));

                return typeToDeserialize;
            }
        }

        /// <summary>
        /// Gets the value of a public property for an object
        /// </summary>
        /// <param name="dest">The dest.</param>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        public static object GetProperty(this object dest, string name)
        {
            if (dest == null)
                throw new ArgumentNullException("dest");

            return dest.GetType().InvokeMember(name, BindingFlags.GetProperty, null, dest, null);
        }

        /// <summary>
        /// Gets the value of a public property for an object
        /// </summary>
        /// <param name="dest">The dest.</param>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        public static T GetProperty<T>(this object dest, string name)
        {
            if (dest == null)
                throw new ArgumentNullException("dest");

            return (T)dest.GetType().InvokeMember(name, BindingFlags.GetProperty, null, dest, null);
        }

        /// <summary>
        /// Gets the field value of an object
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="desc"></param>
        /// <param name="name"></param>
        /// <param name="nonPublic"></param>
        /// <returns></returns>
        public static T GetField<T>(this object dest, string name, bool nonPublic)
        {
            if (dest == null)
                throw new ArgumentNullException("dest");

            BindingFlags flags = BindingFlags.GetField | BindingFlags.Instance | BindingFlags.Public;

            if (nonPublic)
                flags |= BindingFlags.NonPublic;

            return (T)dest.GetType().InvokeMember(name, flags, null, dest, null);
        }

        public static bool IsValidEnumMember(this object e)
        {
            if (e == null)
                return false;
            else
            {
                try
                {
                    return Enum.IsDefined(e.GetType(), e);
                }
                catch 
                {
                    return false;
                }
            }
        }

        /// <summary>
        /// Gets the field value of an object
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="desc"></param>
        /// <param name="name"></param>
        /// <param name="nonPublic"></param>
        /// <returns></returns>
        public static object GetField(this object dest, string name, bool nonPublic)
        {
            if (dest == null)
                throw new ArgumentNullException("dest");

            BindingFlags flags = BindingFlags.GetField | BindingFlags.Instance | BindingFlags.Public;

            if (nonPublic)
                flags |= BindingFlags.NonPublic;

            return dest.GetType().InvokeMember(name, flags, null, dest, null);
        }

        public static void SetProperty(this object dest, string name, object value)
        {
            SetProperty(dest, name, value, false);
        }

        /// <summary>
        /// Sets the value of a public property of the object
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="dest">The dest.</param>
        /// <param name="name">The name.</param>
        public static void SetProperty(this object dest, string name, object value, bool ignoreCase)
        {
            BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy;
            if (ignoreCase)
                flags |= BindingFlags.IgnoreCase;

            if (dest.GetType().GetProperty(name, flags) != null)
            {
                if (dest == null)
                    throw new ArgumentNullException("dest");

                flags = BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy;
                if (ignoreCase)
                    flags |= BindingFlags.IgnoreCase;

                dest.GetType().InvokeMember(name, flags, null, dest, new object[] { value });
            }
        }

        /// <summary>
        /// Populate the interface from the src object
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="src"></param>
        /// <param name="dest"></param>
        public static void CopyObject<T>(this T dest, object src)
        {
            if (dest == null)
                throw new ArgumentNullException("dest");

            if (src == null)
                throw new ArgumentNullException("src");

            foreach (PropertyInfo propertyInfo in dest.GetType().GetProperties())
            {
                //  ok, we can write this one
                if (propertyInfo.CanWrite)
                {
                    PropertyInfo srcProperty = src.GetType().GetProperty(propertyInfo.Name);

                    if (srcProperty != null && srcProperty.CanRead)
                    {
                        //  ok, copy it
                        object val = src.GetType().InvokeMember(srcProperty.Name, BindingFlags.GetProperty, null, src, null);
                        //  and stick it in the bject
                        dest.GetType().InvokeMember(propertyInfo.Name, BindingFlags.SetProperty, null, dest, new object[] { val });
                    }
                }
            }
        }

        /// <summary>
        /// Create a duplicate instance of the entity (shallow copy all the value members, and a reference copy
        /// for al the reference members)
        /// </summary>
        /// <typeparam name="T">The type of the object that is being copied</typeparam>
        /// <param name="entity">The source object to copy</param>
        /// <returns>Returns a new instance of T with its members initialized from a shallow copy of the source</returns>
        public static T ShallowDuplicate<T>(this T entity)
            where T : class
        {
            //  must have a default constructor
            T duplicate = (T)Activator.CreateInstance(entity.GetType());

            foreach (PropertyInfo property in entity.GetType().GetProperties())
            {
                if (property.CanRead && property.CanWrite)
                {
                    //  ok, time to duplicate me
                    object val = entity.GetType().InvokeMember(property.Name, BindingFlags.GetProperty, null, entity, null);
                    //  and set
                    duplicate.GetType().InvokeMember(property.Name, BindingFlags.SetProperty, null, duplicate, new object[] { val });
                }
            }

            return duplicate;
        }

        /// <summary>
        /// Serialize the object into base64
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <returns></returns>
        public static string Base64Serialize(this object entity)
        {
            if (entity == null)
                return null;

            BinaryFormatter formatter = new BinaryFormatter();

            using (MemoryStream ms = new MemoryStream())
            {
                formatter.Serialize(ms, entity);
                ms.Seek(0, SeekOrigin.Begin);
                byte[] buffer = new byte[ms.Length];
                ms.Read(buffer, 0, (int)ms.Length);
                return Convert.ToBase64String(buffer);
            }
        }

        /// <summary>
        /// Serialize the object into base64
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <returns></returns>
        public static string Base64SerializeUsingXml<T>(this T entity)
            where T : class
        {
            if (entity == null)
                return null;

            XmlSerializer formatter = null;
            var types = "";
            if (entity is ISerializationTypeDiscovery)
            {
                var discovery = entity as ISerializationTypeDiscovery;
                formatter = new XmlSerializer(discovery.GetTypesForSerializer().First(), discovery.GetTypesForSerializer().Skip(1).ToArray());
                foreach(var type in discovery.GetTypesForSerializer())
                {
                    types += type.AssemblyQualifiedName + "|";
                }
            }
            else if (entity is IEnumerable)
            {
                List<Type> arrayTypes = new List<Type>();
                arrayTypes.Add(entity.GetType());
                foreach (var i in (IEnumerable)entity)
                {
                    var discovery = i as ISerializationTypeDiscovery;
                    if (discovery != null)
                        arrayTypes.AddRange(discovery.GetTypesForSerializer().Where(t => !arrayTypes.Contains(t)));
                }
                formatter = new XmlSerializer(arrayTypes.First(), arrayTypes.Skip(1).ToArray());
                foreach (var type in arrayTypes)
                {
                    types += type.AssemblyQualifiedName + "|";
                }
            }
            else
            {
                formatter = new XmlSerializer(entity.GetType());
                types = entity.GetType().AssemblyQualifiedName + "|";
            }

            using (MemoryStream ms = new MemoryStream())
            {
                formatter.Serialize(ms, entity);
                ms.Seek(0, SeekOrigin.Begin);
                byte[] buffer = new byte[ms.Length];
                ms.Read(buffer, 0, (int)ms.Length);
                return types + Convert.ToBase64String(buffer);
            }
        }

        /// <summary>
        /// Serialize the object into base64
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <returns></returns>
        public static string Base64SerializeUsingXml(this object entity, params Type [] extraTypes)
        {
            if (entity == null)
                return null;

            XmlSerializer formatter = null;
            var types = "";
            if (entity is ISerializationTypeDiscovery)
            {
                var discovery = entity as ISerializationTypeDiscovery;
                formatter = new XmlSerializer(discovery.GetTypesForSerializer().First(), discovery.GetTypesForSerializer().Skip(1).ToArray());
                foreach (var type in discovery.GetTypesForSerializer())
                {
                    types += type.AssemblyQualifiedName + "|";
                }
            }
            else if (entity is IEnumerable)
            {
                List<Type> arrayTypes = new List<Type>();
                arrayTypes.Add(entity.GetType());
                foreach (var i in (IEnumerable)entity)
                {
                    var discovery = i as ISerializationTypeDiscovery;
                    if (discovery != null)
                        arrayTypes.AddRange(discovery.GetTypesForSerializer().Where(t => !arrayTypes.Contains(t)));
                }
                if (extraTypes != null)
                    arrayTypes.AddRange(extraTypes);
                formatter = new XmlSerializer(arrayTypes.First(), arrayTypes.Skip(1).ToArray());
                foreach(var type in arrayTypes)
                {
                    types += type.AssemblyQualifiedName + "|";
                }
            }
            else
            {
                formatter = new XmlSerializer(entity.GetType());
                types = entity.GetType().AssemblyQualifiedName + "|";
            }

            using (MemoryStream ms = new MemoryStream())
            {
                formatter.Serialize(ms, entity);
                ms.Seek(0, SeekOrigin.Begin);
                byte[] buffer = new byte[ms.Length];
                ms.Read(buffer, 0, (int)ms.Length);
                return types + "|" + Convert.ToBase64String(buffer);
            }
        }

        /// <summary>
        /// Deserialize the object from abase64
        /// </summary>
        /// <param name="base64">The base64.</param>
        /// <returns></returns>
        public static T Base64DeserializeUsingXml<T>(this string base64)
            where T : class
        {

            if (string.IsNullOrEmpty(base64))
                return null;

            var splits = base64.Split('|');
            List<Type> types = new List<Type>();
            foreach(var split in splits)
            {
                if(split != splits.Last())
                    types.Add(Type.GetType(split));
            }

            XmlSerializer formatter = null;
            if(types.Count == 1)
                formatter = new XmlSerializer(types.First());
            else
            {
                formatter = new XmlSerializer(types.First(), types.Skip(1).ToArray());
            }

            byte[] buffer = Convert.FromBase64String(splits.Last());


            using (MemoryStream ms = new MemoryStream(buffer))
            {
                return formatter.Deserialize(ms) as T;
            }
        }

        /// <summary>
        /// Deserialize the object from abase64
        /// </summary>
        /// <param name="base64">The base64.</param>
        /// <returns></returns>
        public static object Base64DeserializeUsingXml(this string base64, params Type [] extraTypes)
        {
            if (string.IsNullOrEmpty(base64))
                return null;

            var splits = base64.Split('|');
            List<Type> types = new List<Type>();
            foreach (var split in splits)
            {
                if (split != splits.Last())
                    types.Add(Type.GetType(split));
            }

            if (extraTypes != null)
                types.AddRange(extraTypes);

            XmlSerializer formatter = null;
            formatter = GetXmlSerializer(types);

            byte[] buffer = Convert.FromBase64String(splits.Last());
            using (MemoryStream ms = new MemoryStream(buffer))
            {
                return formatter.Deserialize(ms);
            }
        }

        /// <summary>
        /// Serializer cache
        /// </summary>
        /// <param name="types"></param>
        /// <returns></returns>
        private static XmlSerializer GetXmlSerializer(List<Type> types)
        {
            types = types.Where(t => t != null).ToList();
            //  generate the key
            StringBuilder key = new StringBuilder();
            foreach (var type in types)
            {
                key.AppendFormat("{0},", type.FullName);
            }
            lock (formatters)
            {
                if (formatters.ContainsKey(key.ToString()))
                    return formatters[key.ToString()];
                else
                {
                    XmlSerializer formatter;
                    if (types.Count == 1)
                        formatter = new XmlSerializer(types.First());
                    else
                    {
                        formatter = new XmlSerializer(types.First(), types.Skip(1).ToArray());
                    }

                    formatters.Add(key.ToString(), formatter);

                    return formatter;
                }
            }
        }


        /// <summary>
        /// Deserialize the object from abase64
        /// </summary>
        /// <param name="base64">The base64.</param>
        /// <returns></returns>
        public static object Base64Deserialize(this string base64, Assembly parent)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Binder = new AllowAllAssemblyVersionsDeserializationBinder(parent);

            if (string.IsNullOrEmpty(base64))
                return null;

            byte[] buffer = Convert.FromBase64String(base64);

            using (MemoryStream ms = new MemoryStream(buffer))
            {
                return formatter.Deserialize(ms);
            }
        }

        public static string ToString(this object src, string defaultString)
        {
            if (src == null)
                return defaultString;
            else
                return src.ToString();
        }

        public static int ToInt32(this Guid g)
        {
            //  get the bytes
            byte[] bytes = g.ToByteArray();

            return BitConverter.ToInt32(bytes, 0);
        }


        public static Guid ToGuid(this int i)
        {
            return new Guid(i, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
        }

        public static void TraceProperties(this object src)
        {
            foreach (PropertyInfo pi in src.GetType().GetProperties())
            {
                Trace.WriteLine(pi.Name + ": " + src.GetProperty(pi.Name));
            }
        }
    }
}
